/*   Copyright (C) 2008  Jeroen v. Schagen & Youri Lima
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;
using System.Net.Sockets;

namespace Engine.Connection.Server
{
    /// <summary>
    ///     Transmission Control Protocol Server
    /// </summary>
    public class TCPServer : IServer
    {
        protected bool listening = false;

        protected Logger logger;
        protected Socket socket;
        protected ArrayList clients;
        
        protected AsyncCallback callback;
        protected IAsyncResult result;

        public event ClientEventHandler ClientConnect;
        public event ClientEventHandler ClientDisconnect;
        public event PackageEventHandler DataRecieved;
        public event StatusEventHandler StatusChanged;

        /// <summary>
        ///     Construct a socket server.
        /// </summary>
        /// 
        /// <param name="name">Identifier</param>
        public TCPServer(string name)
        {
            // Initialize attributes
            logger = new Logger(name);
            clients = new ArrayList();
        }

        /// <summary>
        ///     Start the server, by listening to a specific port number.
        /// </summary>
        /// 
        /// <param name="port"></param>
        /// <returns></returns>
        public bool Listen(int port)
        {
            try
            {
                // Create socket
                socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                // Bind socket to port
                socket.Bind(new IPEndPoint(IPAddress.Any, port));

                // Start listening
                socket.Listen(200);

                // Change status
                listening = true;

                // Trigger status change event
                if (StatusChanged != null)
                    StatusChanged(this);
                
                // Start waiting for connects
                socket.BeginAccept(new AsyncCallback(OnClientConnect), null);

                return true;
            }

            catch (SocketException se)
            {
                // Log exception
                logger.Log(se.ToString(), LogType.ERROR);

                return false;
            }
        }

        /// <summary>
        ///     Close the server.
        /// </summary>
        /// 
        /// <returns></returns>
        public void Close()
        {
            try
            {
                foreach(User client in clients)
                {
                    client.Socket.Close();
                }

                // Clear collection
                clients.Clear();

                if (socket != null)
                {
                    // Close connection
                    socket.Close();

                    // Change status
                    listening = false;
                }

                // Trigger status change event
                if (StatusChanged != null)
                    StatusChanged(this);
            }

            catch (SocketException se)
            {
                logger.Log(se.ToString(), LogType.ERROR);
            }
        }

        /// <summary>
        ///     Disconnect a specific client.
        /// </summary>
        /// 
        /// <param name="client"></param>
        public void Disconnect(User client)
        {
            try
            {
                // Disconnect client
                if(client.Socket != null)
                    client.Socket.Close();

                // Trigger client disconnect event
                if (ClientDisconnect != null)
                    ClientDisconnect(this, client);

                // Remove from collection
                clients.Remove(client);
            }

            catch (SocketException se)
            {
                logger.Log(se.ToString(), LogType.ERROR);
            }
        }

        /// <summary>
        ///     Send a byte array to all connected clients.
        /// </summary>
        /// 
        /// <param name="datastream"></param>
        public void Send(byte[] data)
        {
            foreach (User client in clients)
            {
                Send(data, client);
            }
        }
        
        /// <summary>
        ///     Send a stream to all connected clients.
        /// </summary>
        /// 
        /// <param name="datastream"></param>
        public void Send(Stream datastream)
        {
            foreach (User client in clients)
            {
                Send(datastream, client);
            }
        }

        /// <summary>
        ///     Send byte array to a specific client.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="reciever"></param>
        /// <returns></returns>
        public int Send(byte[] data, User client)
        {
            try
            {
                // Send data over Socket
                return client.Socket.Send(data);
            }

            catch (SocketException se)
            {
                // Log exception
                logger.Log(se.ToString(), LogType.ERROR);

                return -1;
            }
        }

        /// <summary>
        ///     Send a stream to a specific client.
        /// 
        ///     Header indication, just random numbers for now. need logic?
        ///     Space for extra commands or messages etc?
        ///     Size of total package packed in 4 bytes. max is 9 Eb (Exabyte = 1000000 Tb) should be enough :P
        /// </summary>
        /// 
        /// <param name="stream">Stream</param>
        /// <param name="client">Reciever</param>
        public void Send(Stream stream, User client)
        {
            StreamReader sr = new StreamReader(stream);
            long l = stream.Length;

            byte[] header = new byte[] 
            { 
                23 , 200, 123, 65, 0, 0, 0, 0
            };

            // Convert a long to 4 bytes.
            for (int i = 0; i < 4; i++)
            {
                header[7 - i] = (l > byte.MaxValue) ? byte.MaxValue : (byte)l;
                if (l < byte.MaxValue) break;
                l = l >> 8;
            }

            // Send header
            Send(header, client);

            byte[] buffer = new byte[512];
            int pos = 0;
            int result;

            while ((result = stream.ReadByte()) != -1)
            {
                buffer[pos] = (byte)result;
                pos++;

                if (pos == buffer.Length)
                {
                    pos = 0;
                    int sent;

                    if ((sent = Send(buffer, client)) != buffer.Length)
                    {
                        stream.Position -= buffer.Length - sent;
                    }
                }
            }
        }

        /// <summary>
        ///     Blocking mechanism, wait for data.
        /// </summary>
        /// 
        /// <param name="client">Client</param>
        public void WaitForData(User client)
        {
            try
            {
                if (callback == null)
                {
                    callback = new AsyncCallback(OnDataReceived);
                }

                // Initiate package
                Packet package = new Packet(client);

                // Recieve result
                result = client.Socket.BeginReceive(package.DataBuffer, 0, package.DataBuffer.Length, SocketFlags.None, OnDataReceived, package);
            }

            catch (Exception e)
            {
                logger.Log(e.ToString(), LogType.ERROR);
            }
        }

        /// <summary>
        ///     Callback, on client connect.
        /// </summary>
        /// 
        /// <param name="asyn">IAsyncResult</param>
        public void OnClientConnect(IAsyncResult asyn)
        {
            User client = new User();

            try
            {
                // Retrieve client
                client.Socket = socket.EndAccept(asyn);

                // Save socket
                clients.Add(client);

                // Trigger client connect event
                if (ClientConnect != null)
                    ClientConnect(this, client);

                // Let client wait for data
                WaitForData(client);

                // Free callback
                socket.BeginAccept(new AsyncCallback(OnClientConnect), null);
            }

            catch (ObjectDisposedException)
            {
                // Failed to connect
                Disconnect(client);
            }
        }

        /// <summary>
        ///     Callback, on data recieved.
        /// </summary>
        /// 
        /// <param name="asyn">IAsyncResult</param>
        public void OnDataReceived(IAsyncResult asyn)
        {
            Packet package = null;

            try
            {
                // Initiate package
                package = (Packet) asyn.AsyncState;

                // End asynchronous read
                int length = package.User.Socket.EndReceive(asyn);

                // Trim buffer
                byte[] buffer = new byte[length];
                for (int i = 0; i < length; i++) { buffer[i] = package.DataBuffer[i]; }

                package.DataBuffer = buffer;

                // Trigger data recieved event
                if (DataRecieved != null)
                    DataRecieved(this, package);

                // Wait for next package from client
                WaitForData(package.User);
            }

            catch (ObjectDisposedException)
            {
                // Cannot access a disposed object.
            }

            catch (SocketException)
            {
                // Connection to client lost
                Disconnect(package.User);
            }
        }

        /// <summary>
        ///     Retrieve connected clients.
        /// </summary>
        public ArrayList Clients
        {
            get { return clients; }
        }

        /// <summary>
        ///     Retrieve server status.
        /// </summary>
        public bool Connected
        {
            get { return listening; }
        }

        public override string ToString()
        {
            return "Server - online: " + Connected + ", clients: " + Clients.Count;
        }
    }
}
